"""
The problem I have:

A scrolled frame containing hundreds of frames.

Regular updates to all frames kills performance, however only the visible
widgets need actual updates. widgets not on the screen only need
to maintain their data.

Assumption: all frames are same size. (So its easy to work out whats on screen)

My aim here is to monitor which widgets are visible on the screen.
So that update speed can be as fast as possible.

The solution is a scrolled frame that knows whats on the screen.

I get a 10x speed up on:
Mac Book OS X Version 10.5.2 2 GHz Intel Core 2 Duo
"""

from time import time
from math import floor, ceil

from Tkconstants import *
from Tkinter import Tk, Frame, Label, Button
from tkscrolledframe import ScrolledFrame

OPTIMISE = True # set False to see improvement

class iScrolledFrame(ScrolledFrame):
   """
   Intellegent scrolled frame which keeps low and high indexes of frames on screen.
   """

   def __init__(self, parent, widget_class, *a, **k):
      """
      Bind configure event to catch changes in frame size.
      parent is parent widget
      widget_class holds .count attribute for number of widgets on display.
      """
      self.widget_class = widget_class
      ScrolledFrame.__init__(self, parent, *a, **k)
      self.bind('<Configure>', self._configure_event)
      self.low = self.high = 0

   def _configure_event(self, event):
      """
      Catch size changes:
      Simply recalculate the range (after the new sizes have been calculated)
      """
      self.after(200, self._calc_range)

   def _calc_range(self):
      """
      Calculate low from lowest minus a bit.
      Calculate high from highest plus a bit.
      """
      low, high = ScrolledFrame.yview(self)
      diff = (high-low)/2.
      count = self.widget_class.count
      self.low = max(0, floor((low-diff)*count))
      self.high = min(count, ceil((high+diff)*count))

   def yview(self, *a, **k):
      """
      Catch changes in scroll .
      Simply recalculate the range (after the new sizes have been calculated)
      """
      result = ScrolledFrame.yview(self, *a, **k)
      self.after(200, self._calc_range)
      
class Widget(Frame):
   """
   Widgets to add to the scroll frame.
   """
   db = [] #list of frames
   count = 0 # count of frames
   updates = 0 # count of updates to monitor performance
   
   def __init__(self, parent):
      """
      Create widgets for each frame.
      """
      Widget.count += 1
      Widget.db.append(self)
      Frame.__init__(self, parent.interior(), relief=SUNKEN, bd=4)
      self.sf = parent
      self.val = Widget.count
      self.create_widgets()

   def create_widgets(self):
      """
      A label and a few buttons.
      """
      self.index = self.val
      Label(self, text=str(self.val), width=10).grid(row=1, column=1)
      self.buttons = []
      for row in range(3):
         for col in range(5):
            w = Button(self, width=8)
            w.grid(row=1+row,column=2+col)
            self.buttons.append(w)

   def refresh(self):
      """
      Always store the data for he widget.
      Only update the actual widget if its on the screen.
      """
      Widget.updates += 1
      self.val += 1
      sf = self.sf
      if not OPTIMISE or (sf.low <= self.index <= sf.high):
         for idx, obj in enumerate(self.buttons):
            obj.configure(text=self.val+idx)


if __name__ == '__main__':
   class Application(Tk):
      """
      Demo of putting it all together.
      """

      def __init__(self):
         """
         Create the scrolled frame containing lots of frames.
         Start timing after 1 second to allow all widgets to be created.
         """
         Tk.__init__(self)
         self.geometry('600x500+100+100')
         self.title('iScrolledFrame Demo')
         sf  = self.sf = iScrolledFrame(self, Widget)
         sf.pack(expand=YES, fill=BOTH)
         for iii in range(256):
            Widget(sf).grid(sticky=W+E)

         self.start = time()+1
         self.t = self.start
         self.after(1000, self.update_all)

      def update_all(self):
         """
         Every second report the number of updaes.
         When enough have been done ... stop.
         On each iteration update all widgets.
         """
         t = time()
         if t - self.t > 1:
            self.t = t
            if Widget.updates>10000:
               print 'update/sec=' ,Widget.updates / (t - self.start)
               self.quit()
               return
            sf = self.sf
            print Widget.updates, sf.low, sf.high, sf.high - sf.low

         for widget in Widget.db:
            widget.refresh()
         root.after(10, self.update_all)
         
   root=Application()
   root.mainloop()
